﻿/*
Copyright 2011 Google Inc.

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

      http://www.apache.org/licenses/LICENSE-2.0

 Unless required by applicable law or agreed to in writing, software
 distributed under the License is distributed on an "AS IS" BASIS,
 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 See the License for the specific language governing permissions and
 limitations under the License.

 Decryption and signature verification for the Google Payment Express API.

 Authors:
 Himanshu Barthwal (himabar@google.com)
 Manish Sharma     (smanish@google.com)
 Shawn Willden     (swillden@google.com)
 Nadim Ratani      (nratani@google.com)
*/

using System;
using System.Collections.Generic;
using System.Text;
using System.Security.Cryptography.X509Certificates;
using System.Security.Cryptography;
using System.IO;
using Newtonsoft.Json;
using ASNParsing;

namespace google.paymentexpress
{
    /// <summary>
    /// Performs both decryption and signature verification.
    /// </summary>
    public class Decrypter
    {
        private Encoding encoding = Encoding.UTF8;

        // Path to your private RSA key in BLOB format
        private string merchantPrivateKeyFilePath;

        // Path to Google's public DSA key in BLOB format
        private string googlePublicKeyFilePath;

        // Fields corresponding to encrypted data in the full wallet response JWT
        private SessionData sessionData;
        private FieldData fieldData;

        /// <summary>
        /// Constructor takes both key locations in BLOB format as a parameter
        /// </summary>
        public Decrypter(string merchantPrivateKeyFilePath, string googlePublicKeyFilePath)
        {
            this.merchantPrivateKeyFilePath = merchantPrivateKeyFilePath;
            this.googlePublicKeyFilePath = googlePublicKeyFilePath;
        }

        /// <summary>
        /// Decrypts and verifies the payload with corresponding session and field data.
        /// Returns the plaintext.
        /// </summary>
        public string Decrypt(string sessionData, string fieldData)
        {
            this.sessionData = GetSessionData(sessionData);
            this.fieldData = GetFieldData(fieldData);
            return GetPlainText();
        }

        /// <summary>
        /// Decodes and decrypts the session data.
        /// </summary>
        private SessionData GetSessionData(string encodedSessionData)
        {
            byte[] jsonSessionData = null;
            SessionData sessionDataObject = null;
            try
            {
                byte[] encryptedSessionData = DecodeFromBase64(encodedSessionData);
                byte[] version = new byte[1];
                byte[] sha1 = new byte[4];
                byte[] data = new byte[256];

                Array.Copy(encryptedSessionData, 0, version, 0, 1);
                Array.Copy(encryptedSessionData, 1, sha1, 0, 4);
                Array.Copy(encryptedSessionData, 5, data, 0, 256);

                CspParameters rsaParams = new CspParameters();
                rsaParams.Flags = CspProviderFlags.UseMachineKeyStore;
                RSACryptoServiceProvider provider = new RSACryptoServiceProvider(4096, rsaParams);
                byte[] merchantPrivateKey = File.ReadAllBytes(merchantPrivateKeyFilePath);
                provider.ImportCspBlob(merchantPrivateKey);
                jsonSessionData = provider.Decrypt(data, true);
                sessionDataObject = (SessionData)JsonConvert.DeserializeObject(encoding.GetString(jsonSessionData), typeof(SessionData));
            }
            catch (Exception ex)
            {
                Console.WriteLine("Error encountered while decrypting session data:" + ex.Message);
            }
            return sessionDataObject;
        }

        /// <summary>
        /// Decodes the field data.
        /// </summary>
        private FieldData GetFieldData(string decodedFieldData)
        {
            try
            {
                byte[] data = DecodeFromBase64(decodedFieldData);
                byte[] dataLengthString = new byte[4];
                Array.Copy(data, 5, dataLengthString, 0, 4);
                int dataLength = ReadBigEndianInteger(dataLengthString, 0);
                FieldData fieldData = new FieldData();
                fieldData.dataLength = dataLength;
                fieldData.sha1 = new byte[4];
                fieldData.version = new byte[1];
                fieldData.signaturedData = new byte[dataLength];
                fieldData.signature = new byte[data.Length - dataLength - 9];
                Array.Copy(data, 0, fieldData.version, 0, 1);
                Array.Copy(data, 1, fieldData.sha1, 0, 4);
                Array.Copy(data, 9, fieldData.signaturedData, 0, dataLength);
                Array.Copy(data, dataLength + 9, fieldData.signature, 0, fieldData.signature.Length);

                return fieldData;
            }
            catch (Exception ex)
            {
                Console.WriteLine("Error encountered while getting fieldData : " + ex.Message);
            }
            return null;
        }

        /// <summary>
        /// Decrypts the data and returns the plaintext.
        /// </summary>
        private string GetPlainText()
        {
            int offset = 0;
            byte version = fieldData.signaturedData[offset++];

            byte[] keyHash = new byte[4];
            Array.Copy(fieldData.signaturedData, offset, keyHash, 0, keyHash.Length);
            offset += keyHash.Length;

            byte[] initialVector = new byte[sessionData.key.size / 8];
            Array.Copy(fieldData.signaturedData, offset, initialVector, 0, initialVector.Length);
            offset += initialVector.Length;

            byte[] encryptedData = new byte[fieldData.dataLength - (offset + 20)];
            Array.Copy(fieldData.signaturedData, offset, encryptedData, 0, encryptedData.Length);
            offset += encryptedData.Length;

            byte[] hmac = encoding.GetBytes(sessionData.key.hmacKey.hmacKeyString);

            byte[] b64AesKey = DecodeFromBase64(sessionData.key.aesKeyString);
            if (IsSignatureValid())
            {
                return Aes128CbcDecrypt(b64AesKey, encryptedData, initialVector);
            }
            else
            {
                throw new Exception("Signature is not valid.");
            }
        }

        /// <summary>
        /// Removes invalid base 64 characters to make it web safe.
        /// </summary>
        private string RemoveInvalidBase64Characters(string data)
        {
            return data.Replace("-", "+").Replace("_", "/");
        }

        private static byte[] FormatValue(byte[] value)
        {
            byte[] formattedValue = new byte[20];
            int startPos = Math.Max(0, value.Length - 20);
            int destPos = Math.Max(0, 20 - value.Length);
            int toCopy = Math.Min(20, value.Length);
            Array.Copy(value, startPos, formattedValue, destPos, toCopy);
            return formattedValue;
        }

        private string Aes128CbcDecrypt(byte[] b64AesKey, byte[] encryptedData, byte[] initialVector)
        {
            RijndaelManaged myRijndael = new RijndaelManaged();
            byte[] plainText = new byte[encryptedData.Length];
            ICryptoTransform decrypter = myRijndael.CreateDecryptor(b64AesKey, initialVector);
            decrypter.TransformBlock(encryptedData, 0, encryptedData.Length, plainText, 0);

            // Work around .NET crypto library bug.  If you're doing CBC-mode decryption but only
            // decrypt a single block, it gives you the IV pre-image rather than the data.  By
            // decrypting a second time (same data!) you actually get the plaintext.
            if (encryptedData.Length == 16)
            {
                decrypter.TransformBlock(encryptedData, 0, encryptedData.Length, plainText, 0);
            }
            string result = encoding.GetString(plainText);
            return result.Trim();
        }

        /// <summary>
        /// Verifies the signature.
        /// </summary>
        private bool IsSignatureValid()
        {
            byte[] nonce = DecodeFromBase64(sessionData.nonce);
            List<byte> dataBuilder = new List<byte>();
            dataBuilder.AddRange(fieldData.signaturedData);
            dataBuilder.AddRange(GetPaddingBytes());
            dataBuilder.AddRange(nonce);
            dataBuilder.Add(BitConverter.GetBytes(0)[0]);//null byte
            byte[] dataToVerify = dataBuilder.ToArray();

            // Building the DSA object to verify signature
            DSACryptoServiceProvider provider = new DSACryptoServiceProvider();
            provider.ImportCspBlob(File.ReadAllBytes(googlePublicKeyFilePath));
            byte[] sigBytes = ConvertToP1363Signature(fieldData.signature);
            DSASignatureDeformatter verifier = new DSASignatureDeformatter(provider);
            verifier.SetHashAlgorithm("SHA1");

            SHA1 sha1 = new SHA1CryptoServiceProvider();
            byte[] hash = sha1.ComputeHash(dataToVerify);

            return verifier.VerifySignature(hash, sigBytes);
        }

        private byte[] GetPaddingBytes()
        {
            byte[] padding = BitConverter.GetBytes(16);
            if (BitConverter.IsLittleEndian)
            {
                Array.Reverse(padding);
            }
            return padding;
        }

        private byte[] ConvertToP1363Signature(byte[] signature)
        {
            AsnParser parser = new AsnParser(fieldData.signature);
            parser.NextSequence();
            byte[] r = FormatValue(parser.NextInteger());
            byte[] s = FormatValue(parser.NextInteger());

            byte[] result = new byte[40];
            Array.Copy(r, 0, result, 0, r.Length);
            Array.Copy(s, 0, result, r.Length, s.Length);
            return result;
        }

        private byte[] DecodeFromBase64(string data)
        {
            int diff = data.Length % 4 ;
            if (diff > 0)
            {
                data = data.PadRight(data.Length + 4 - diff, '=');
            }
            var binaryData = Convert.FromBase64String(RemoveInvalidBase64Characters(data));
            return binaryData;
        }

        private int ReadBigEndianInteger(byte[] bytes, int startIndex)
        {
            Array.Reverse(bytes);
            return BitConverter.ToInt32(bytes, startIndex);
        }

        private class SessionData
        {
            public Key key;
            public string nonce;
        }

        private class Key
        {
            public int size;
            public string aesKeyString;
            public HMACKey hmacKey;
            public string mode;
        }

        private class FieldData
        {
            public byte[] version;
            public byte[] sha1;
            public int dataLength;
            public byte[] signaturedData;
            public byte[] signature;
        }

        private class JsonData
        {
            public string session_data;
            public string field_data;
        }

        private class HMACKey
        {
            public string hmacKeyString;
            public string size;
        }
    }
}
